/*
 *  Copyright 2008 Blandware (http://www.blandware.com)
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
package com.blandware.atleap.antext.core.generate;

import com.blandware.atleap.antext.core.exception.SpecificationParsingException;

import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.HashSet;

/**
 * Type of property of generated entity.
 * <p><a href="PropertyType.java.html"><i>View source</i></a></p>
 *
 * @author Roman Puchkovskiy <a href="mailto:roman.puchkovskiy@blandware.com">
 *         &lt;roman.puchkovskiy@blandware.com&gt;</a>
 * @version $Revision: 1.4 $ $Date: 2008/03/26 18:59:36 $
 */
public class PropertyType implements Cloneable {
    // primitive types
    public static final String TYPE_BOOLEAN = "boolean";
    public static final String TYPE_BYTE = "byte";
    public static final String TYPE_CHAR = "char";
    public static final String TYPE_SHORT = "short";
    public static final String TYPE_INT = "int";
    public static final String TYPE_LONG = "long";
    public static final String TYPE_FLOAT = "float";
    public static final String TYPE_DOUBLE = "double";

    // primitive WRAPPER types
    public static final String TYPE_BOOLEAN_WRAPPER = "Boolean";
    public static final String TYPE_BYTE_WRAPPER = "Byte";
    public static final String TYPE_CHAR_WRAPPER = "Character";
    public static final String TYPE_SHORT_WRAPPER = "Short";
    public static final String TYPE_INT_WRAPPER = "Integer";
    public static final String TYPE_LONG_WRAPPER = "Long";
    public static final String TYPE_FLOAT_WRAPPER = "Float";
    public static final String TYPE_DOUBLE_WRAPPER = "Double";

    // common types (almost primitives as supported natively by most DBs)
    public static final String TYPE_STRING = "string";
    public static final String TYPE_DATE = "date";

    // Hybernate special types
    public static final String TYPE_ID = "id";
    public static final String TYPE_VERSION = "version";

    // BLOB
    public static final String TYPE_BLOB = "blob";

    // Hibernate collections and so on
    public static final String TYPE_BAG = "bag";
    public static final String TYPE_LIST = "list";
    public static final String TYPE_MAP = "map";
    public static final String TYPE_MANY_TO_ONE = "many-to-one";

    // AtLeap special types
    public static final String TYPE_MULTI_STRING = "multi-string";
    // Field-backed types
    public static final String TYPE_FIELD_LINE = "field-line";
    public static final String TYPE_FIELD_MULTILINE = "field-multiline";
    public static final String TYPE_FIELD_HTML = "field-html";

    private String name;
    private boolean complex;
    private String javaType;
    private String hibernateType;
    private String element;

    // all known (predefined) types; complex types are actually prototypes
    // as their element is not set here
    private static final Map types = new HashMap();
    static {
        initPrimitiveType(TYPE_BOOLEAN, "true_false");
        initPrimitiveType(TYPE_BYTE);
        initPrimitiveType(TYPE_CHAR);
        initPrimitiveType(TYPE_SHORT);
        initPrimitiveType(TYPE_INT);
        initPrimitiveType(TYPE_LONG);
        initPrimitiveType(TYPE_FLOAT);
        initPrimitiveType(TYPE_DOUBLE);

        initWrapperType(TYPE_BOOLEAN_WRAPPER, "true_false");
        initWrapperType(TYPE_BYTE_WRAPPER);
        initWrapperType(TYPE_CHAR_WRAPPER);
        initWrapperType(TYPE_SHORT_WRAPPER);
        initWrapperType(TYPE_INT_WRAPPER);
        initWrapperType(TYPE_LONG_WRAPPER);
        initWrapperType(TYPE_FLOAT_WRAPPER);
        initWrapperType(TYPE_DOUBLE_WRAPPER);

        initType(TYPE_STRING, false, "String", null);
        initType(TYPE_DATE, false, "Date", "date");

        initType(TYPE_ID, false, "Long", null);
        initType(TYPE_VERSION, false, "Long", "long");

        initType(TYPE_BLOB, false, "byte[]", null);

        initComplexType(TYPE_BAG, "List");
        initComplexType(TYPE_LIST, "List");
        initComplexType(TYPE_MAP, "Map");
        initComplexType(TYPE_MANY_TO_ONE, null);

        initType(TYPE_MULTI_STRING, false, "Map", null);
        initType(TYPE_FIELD_LINE, false, "Map", null);
        initType(TYPE_FIELD_MULTILINE, false, "Map", null);
        initType(TYPE_FIELD_HTML, false, "Map", null);
    }

    // names of types which correspond to Java primitive types
    private static final Set javaPrimitiveTypeNames = new HashSet();
    static {
        javaPrimitiveTypeNames.add(TYPE_BOOLEAN);
        javaPrimitiveTypeNames.add(TYPE_BYTE);
        javaPrimitiveTypeNames.add(TYPE_CHAR);
        javaPrimitiveTypeNames.add(TYPE_SHORT);
        javaPrimitiveTypeNames.add(TYPE_INT);
        javaPrimitiveTypeNames.add(TYPE_LONG);
        javaPrimitiveTypeNames.add(TYPE_FLOAT);
        javaPrimitiveTypeNames.add(TYPE_DOUBLE);
    }

    // names of types which correspond to classes representing wrappers of
    // Java primitive types
    private static final Set javaWrapperTypeNames = new HashSet();
    static {
        javaWrapperTypeNames.add(TYPE_BOOLEAN_WRAPPER);
        javaWrapperTypeNames.add(TYPE_BYTE_WRAPPER);
        javaWrapperTypeNames.add(TYPE_CHAR_WRAPPER);
        javaWrapperTypeNames.add(TYPE_SHORT_WRAPPER);
        javaWrapperTypeNames.add(TYPE_INT_WRAPPER);
        javaWrapperTypeNames.add(TYPE_LONG_WRAPPER);
        javaWrapperTypeNames.add(TYPE_FLOAT_WRAPPER);
        javaWrapperTypeNames.add(TYPE_DOUBLE_WRAPPER);
    }

    /**
     * Private constructor.
     *
     * @param name          type name
     * @param complex       whether type is complex
     * @param javaType      type to be used in Java
     * @param hibernateType type to be used in hibernatedoclet annotation; if
     * null, no type will be rendered
     */
    private PropertyType(String name, boolean complex, String javaType,
                         String hibernateType) {
        this.name = name;
        this.complex = complex;
        this.javaType = javaType;
        this.hibernateType = hibernateType;
    }

    /**
     * Returns type name.
     *
     * @return name
     */
    public String getName() {
        return name;
    }

    /**
     * Returns whether type is complex. If true, its element has sense.
     *
     * @return true if complex
     */
    public boolean isComplex() {
        return complex;
    }

    /**
     * Returns string representing name of Java type corresponding to this type
     * in Java code (i.e. "Long" for Long, without package).
     *
     * @return java type
     */
    public String getJavaType() {
        return javaType;
    }

    /**
     * Returns string representing name of Hibernate type which will be used
     * to represent this type. If this is null, Hibernate will determine type
     * automatically.
     *
     * @return hibernate type
     */
    public String getHibernateType() {
        return hibernateType;
    }

    /**
     * Returns true if hibernate type is not null.
     *
     * @return true if hibernate type is not null
     */
    public boolean isHibernateTypeSet() {
        return hibernateType != null;
    }

    /**
     * Returns this type element.
     *
     * @return element
     */
    public String getElement() {
        return element;
    }

    /**
     * Sets this type element.
     *
     * @param element   element to set
     */
    public void setElement(String element) {
        this.element = element;
    }

    /**
     * Returns true if this type is primitive from the point of view of Java.
     *
     * @return true if java-primitive
     */
    public boolean isJavaPrimitive() {
        return javaPrimitiveTypeNames.contains(name);
    }

    /**
     * Returns true if property of this type will be used to generate equals()
     * and hashCode() if no explicit list is given.
     *
     * @return true if candidate
     */
    public boolean isDefaultEqualsCandidate() {
        return !TYPE_ID.equals(name) && !TYPE_VERSION.equals(name);
    }

    /**
     * Returns true if Java representation of this type is array.
     *
     * @return true if array
     */
    public boolean isJavaTypeArray() {
        return javaType.endsWith("[]");
    }

    /**
     * Returns true if this is type which uses field, not Hibernate properties.
     *
     * @return true if field-backed
     */
    public boolean isFieldBacked() {
        return TYPE_FIELD_LINE.equals(name) || TYPE_FIELD_MULTILINE.equals(name)
                || TYPE_FIELD_HTML.equals(name); 
    }

    /**
     * Returns true if type with given name is primitive from the point of
     * view of Hibernate (i.e. it's stored in one column).
     *
     * @param typeName  name of type
     * @return true if primitive
     */
    public static boolean isHibernatePrimitive(String typeName) {
        // TODO: reimplement if needed
        // this method relies on fact that all 'known' non-complex types are
        // Hibernate primitives
        boolean result = false;
        if (types.containsKey(typeName)) {
            result = !((PropertyType) types.get(typeName)).isComplex();
        }
        return result;
    }

    /**
     * Returns true if this type is primitive from the point of
     * view of Hibernate (i.e. it's stored in one column).
     *
     * @return true if primitive
     */
    public boolean isHibernatePrimitive() {
        return isHibernatePrimitive(name);
    }

    /**
     * Returns true if this type's element is primitive from the point of
     * view of Hibernate (i.e. it's stored in one column).
     *
     * @return true if element is primitive
     */
    public boolean isElementHibernatePrimitive() {
        return isHibernatePrimitive(element);
    }

    /**
     * Returns element full type. This is full-qualified name for wrapper types
     * and name for other types.
     *
     * @return full type of element
     */
    public String getElementFullType() {
        if (javaWrapperTypeNames.contains(getElement())) {
            return "java.lang." + getElement();
        } else {
            return getElement();
        }
    }

    /**
     * Returns type with given specification. If it is a known simple type,
     * it's only instance is returned; if it's a known complex type, its
     * definition is used as a prototype (so its copy properly initialized is
     * returned); otherwise a new type is constructed.
     *
     * @param spec  specification of type
     * @return type
     * @throws SpecificationParsingException    if specification is bad
     */
    public static PropertyType getType(String spec)
            throws SpecificationParsingException {
        TypeDefinition typeDefinition = parseTypeDefinition(spec);
        PropertyType type;
        if (typeDefinition.isComplex()) {
            type = (PropertyType) types.get(typeDefinition.getName());
            if (type == null) {
                throw new SpecificationParsingException("Unknown complex type: "
                        + typeDefinition.getName());
            }
            type = (PropertyType) type.clone();
            type.setElement(typeDefinition.getElement());
            // initializing type to element for many-to-one...
            if (TYPE_MANY_TO_ONE.equals(type.getName())) {
                type.javaType = type.getElement();
            }
        } else { 
            type = (PropertyType) types.get(spec);
            if (type == null) {
                // no such type defined, assume it's a user-defined type
                type = new PropertyType(spec, false, spec, null);
            } else if (type.isComplex()) {
                // noway, it's not complex!
                throw new SpecificationParsingException("Element needs to be specified because type is complex: "
                        + type.getName());
            } else {
                // all seems to be ok, just return this type...
            }
        }
        return type;
    }

    /**
     * @see Object#clone() 
     */
    protected Object clone() {
        PropertyType result = new PropertyType(name, complex, javaType,
                hibernateType);
        result.setElement(getElement());
        return result;
    }

    /**
     * Initializes a type putting it to store of known types.
     *
     * @param name          type name
     * @param complex       whether it's complex
     * @param javaType      java type for this type
     * @param hibernateType hibernate type for this type (if null, Hibernate
     * will choose type automatically)
     */
    private static void initType(String name, boolean complex, String javaType,
                         String hibernateType) {
        types.put(name, new PropertyType(name, complex,
                javaType, hibernateType));
    }

    /**
     * Initializes a primitive type putting it to store of known types.
     *
     * @param name          type name
     * @param hibernateType hibernate type for this type (if null, Hibernate
     * will choose type automatically)
     */
    private static void initPrimitiveType(String name, String hibernateType) {
        initType(name, false, name, hibernateType);
    }

    /**
     * Initializes a primitive type putting it to store of known types.
     * Hibernate type is set to null meaning that Hibernate will choose type
     * automatically.
     *
     * @param name          type name
     */
    private static void initPrimitiveType(String name) {
        initPrimitiveType(name, null);
    }

    /**
     * Initializes a wrapper type putting it to store of known types.
     *
     * @param name          type name
     * @param hibernateType hibernate type for this type (if null, Hibernate
     * will choose type automatically)
     */
    private static void initWrapperType(String name, String hibernateType) {
        initType(name, false, name, hibernateType);
    }

    /**
     * Initializes a wrapper type putting it to store of known types.
     * Hibernate type is set to null meaning that Hibernate will choose type
     * automatically.
     *
     * @param name          type name
     */
    private static void initWrapperType(String name) {
        initWrapperType(name, null);
    }

    /**
     * Initializes a complex type putting it to store of known types.
     *
     * @param name          type name
     * @param javaType      java type
     */
    private static void initComplexType(String name, String javaType) {
        initType(name, true, javaType, null);
    }

    /**
     * Parses a type definition.
     *
     * @param spec  type specification
     * @return type definition
     * @throws SpecificationParsingException    if specification is bad
     */
    private static TypeDefinition parseTypeDefinition(String spec)
            throws SpecificationParsingException {
        boolean complex;
        String typeName;
        String element = null;
        if (spec.contains("(")) {
            complex = true;
            String[] fragments = spec.split("\\(");
            if (fragments.length != 2) {
                throw new SpecificationParsingException("Bad complex type:"
                        + spec);
            }
            typeName = fragments[0].trim();
            element = fragments[1].trim();
            if (!element.endsWith(")")) {
                throw new SpecificationParsingException("Bad complex type:"
                        + spec);
            }
            element = element.substring(0, element.length() - 1).trim();
            if (TYPE_ID.equals(element) || TYPE_VERSION.equals(element)) {
                throw new SpecificationParsingException("Complex type cannot have ID or version as element:"
                        + spec);
            }
        } else {
            // this should be a simple type
            complex = false;
            typeName = spec;
        }
        TypeDefinition result;
        if (complex) {
            result = new TypeDefinition(typeName, element);
        } else {
            result = new TypeDefinition(typeName);
        }
        return result;
    }

    /**
     * Structure for type definition.
     */
    private static class TypeDefinition {
        private String name;
        private String element;
        private boolean complex;

        TypeDefinition(String name) {
            this.name = name;
            this.complex = false;
        }

        TypeDefinition(String name, String element) {
            this.name = name;
            this.element = element;
            this.complex = true;
        }

        public String getName() {
            return name;
        }

        public String getElement() {
            return element;
        }

        public boolean isComplex() {
            return complex;
        }
    }
}